package com.nali.dal.mongo.explain.query;

import java.io.Serializable;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.mongodb.BasicDBObject;
import com.nali.dal.exception.SyntaxException;
import com.nali.dal.expression.ExpressionDto;
import com.nali.dal.expression.ExpressionValue;
import com.nali.dal.expression.PropertyExpression;
import com.nali.dal.expression.query.Criteria;
import com.nali.dal.expression.query.CriteriaType;

public class ResultProperties {
	private static final String _ID = "_id";
	private Map<String, Integer> keys;
	private SelectKey selectKey = SelectKey.normal;
	private boolean showId = false;
	private LinkedHashMap<String, Condition> specialKeys;

	public void addSelect(PropertyExpression<Criteria> cv) {
		if (keys == null) {
			keys = new HashMap<String, Integer>();
		}
		Criteria c = cv.getExpression();
		String prop = cv.getPropertyName();
		if (_ID.equals(prop)) {
			if (c == Criteria.select) {
				showId = true;
			} else {
				showId = false;
			}
		} else {
			if (specialKeys != null && specialKeys.containsKey(prop)) {
				throw new SyntaxException("key is in using:" + prop);
			}
			selectKey = selectKey.whenCriteria(c);
			keys.put(prop, c == Criteria.select ? 1 : 0);
		}
	}

	public void checkGroupBy(Set<String> groupBys) throws SyntaxException {
		if (groupBys == null) {
			return;
		}
		if (specialKeys != null) {
			throw new SyntaxException("cannot mix group by and special selecting");
		}
		if (keys != null && keys.size() > 0 && (!groupBys.containsAll(keys.keySet()))) {
			throw new SyntaxException("group by not supported other key");
		}
	}

	public void addSpecialSelect(Criteria select, String prop, Serializable value) {
		switch (select) {
		case elemMatch:
			addElementMatch(prop, value);
			break;
		default:
			throw new SyntaxException("not supported select type:" + select);
		}
	}

	@SuppressWarnings("unchecked")
	private void addElementMatch(String prop, Serializable value) {// TODO 考虑 prop含“.”的情况
		if (keys != null && keys.containsKey(prop)) {
			throw new SyntaxException("key is in using:" + prop);
		}
		if (specialKeys == null) {
			specialKeys = new LinkedHashMap<String, Condition>();
		}
		if (specialKeys.containsKey(prop)) {
			throw new SyntaxException("key is in using:" + prop);
		}
		Condition cond = new Condition();
		// ExpressionDto<Criteria>或List<ExpressionValue>
		if (value instanceof ExpressionDto) {
			ExpressionDto<Criteria> query = (ExpressionDto<Criteria>) value;
			List<ExpressionValue<Criteria>> expressionValues = query.getExpressionValues();
			List<PropertyExpression<Criteria>> propertyExpressions = query.getPropertyExpressions();
			for (ExpressionValue<Criteria> expressionValue : expressionValues) {
				addWhere(cond, expressionValue);
			}
			for (PropertyExpression<Criteria> propertyExpression : propertyExpressions) {
				addWhere(cond, propertyExpression);
			}
		}
		if (value instanceof List) {
			List<ExpressionValue<Criteria>> expressionValues = (List<ExpressionValue<Criteria>>) value;
			for (ExpressionValue<Criteria> expressionValue : expressionValues) {
				addWhere(cond, expressionValue);
			}
		}
		specialKeys.put(prop, cond);
		// TODO
	}

	private void addWhere(Condition cond, PropertyExpression<Criteria> cv) {
		CriteriaType type = cv.getExpression().type;
		switch (type) {
		case where:
			cond.add(cv.getExpression(), cv.getPropertyName());
			break;
		default:
			throw new SyntaxException("not supported type:" + type);
		}
	}

	private void addWhere(Condition cond, ExpressionValue<Criteria> cv) {
		CriteriaType type = cv.getPropertyExpression().getExpression().type;
		switch (type) {
		case where:
			PropertyExpression<Criteria> pc = cv.getPropertyExpression();
			cond.add(pc.getExpression(), pc.getPropertyName(), cv.getValue());
			break;
		default:
			throw new SyntaxException("not supported type:" + type);
		}
	}

	public BasicDBObject getKeys(List<Object> params, int offset) {
		BasicDBObject properties = new BasicDBObject();
		if (!showId) {
			properties.append(_ID, 0);
		}
		if (keys != null) {
			for (String prop : keys.keySet()) {
				properties.append(prop, keys.get(prop));
			}
		}
		if (specialKeys != null) {
			for (String prop : specialKeys.keySet()) {
				Condition cond = specialKeys.get(prop);
				properties.append(prop, cond.getQueryDBObject(params, offset));
				offset += cond.getParamsLength();
			}
		}
		return properties;
	}
}

enum SelectKey {
	normal(true, true), select(true, false), unSelect(false, true), ;
	private boolean canSelectKey = true;
	private boolean canUnSelectKey = true;

	private SelectKey(boolean canSelectKey, boolean canUnSelectKey) {
		this.canSelectKey = canSelectKey;
		this.canUnSelectKey = canUnSelectKey;
	}

	public SelectKey whenCriteria(Criteria c) {
		if (c == Criteria.select) {
			return whenSelectKey();
		} else {
			return whenUnSelectKey();
		}
	}

	public SelectKey whenSelectKey() {
		if (canSelectKey) {
			return select;
		} else {
			throw new SyntaxException("cannot mix selectkey and unselectkey");
		}
	}

	public SelectKey whenUnSelectKey() {
		if (canUnSelectKey) {
			return unSelect;
		} else {
			throw new SyntaxException("cannot mix selectkey and unselectkey");
		}
	}
}